Update dependency kysely to v0.28.17 [SECURITY]#920
Open
renovate[bot] wants to merge 1 commit into
Open
Conversation
4142fcd to
79b61e1
Compare
79b61e1 to
1c51d37
Compare
1c51d37 to
0c27e58
Compare
0c27e58 to
ea8e9e6
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR contains the following updates:
0.28.11→0.28.17SQL Injection via unsanitized JSON path keys when ignoring/silencing compilation errors or using
Kysely<any>.CVE-2026-32763 / GHSA-wmrf-hv6w-mr66
More information
Details
Summary
Kysely through 0.28.11 has a SQL injection vulnerability in JSON path compilation for MySQL and SQLite dialects. The
visitJSONPathLeg()function appends user-controlled values from.key()and.at()directly into single-quoted JSON path string literals ('$.key') without escaping single quotes. An attacker can break out of the JSON path string context and inject arbitrary SQL.This is inconsistent with
sanitizeIdentifier(), which properly doubles delimiter characters for identifiers — both are non-parameterizable SQL constructs requiring manual escaping, but only identifiers are protected.Details
visitJSONPath()wraps JSON path in single quotes ('$...'), andvisitJSONPathLeg()appends each key/index value viathis.append(String(node.value))with no sanitization:Contrast with
sanitizeIdentifier()in the same file, which properly doubles delimiter characters:Both identifiers and JSON path keys are non-parameterizable SQL constructs that require manual escaping. Identifiers are protected; JSON path values are not.
PostgreSQL is not affected. The branching happens in
JSONPathBuilder.#createBuilderWithPathLeg()(json-path-builder.js):->$,->>$) produce aJSONPathNodetraversal →visitJSONPathLeg()concatenates the key directly into a single-quoted JSON path string ('$.key') — vulnerable, no escaping.->,->>) produce aJSONOperatorChainNodetraversal →ValueNode.createImmediate(value)→appendImmediateValue()→appendStringLiteral()→sanitizeStringLiteral()doubles single quotes ('→''), generating chained operators ("col"->>'city'). Injection payload becomes a harmless string literal.Same
.key()call, different internal node creation depending on the operator type. The PostgreSQL path reuses the existing string literal sanitization; the MySQL/SQLite JSON path construction bypasses it entirely.PoC
End-to-end proof against a real SQLite database (Kysely 0.28.11 + better-sqlite3):
The payload includes
as "city" from "users"to complete the first SELECT before the UNION. The--comments out the trailing' as "city" from "users"appended by Kysely.Generated SQL:
Realistic application pattern
Dynamic JSON field selection is a common pattern in search APIs, GraphQL resolvers, and admin panels that expose JSON column data.
Suggested fix
Escape single quotes in JSON path values within
visitJSONPathLeg(), similar to howsanitizeIdentifier()doubles delimiter characters. Alternatively, validate that JSON path keys contain only safe characters. The direction of the fix is left to the maintainers.Impact
SQL Injection (CWE-89) — An attacker can inject arbitrary SQL via crafted JSON key names passed to
.key()or.at(), enabling UNION-based data exfiltration from any database table. MySQL and SQLite dialects are affected. PostgreSQL is not affected.Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:NReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Kysely has a MySQL SQL Injection via Insufficient Backslash Escaping in
sql.lit(string)usage or similar methods that append string literal values into the compiled SQL stringsCVE-2026-33468 / GHSA-8cpq-38p9-67gx
More information
Details
Summary
Kysely's
DefaultQueryCompiler.sanitizeStringLiteral()only escapes single quotes by doubling them ('→'') but does not escape backslashes. When used with the MySQL dialect (whereNO_BACKSLASH_ESCAPESis OFF by default), an attacker can use a backslash to escape the trailing quote of a string literal, breaking out of the string context and injecting arbitrary SQL. This affects any code path that usesImmediateValueTransformerto inline values — specificallyCreateIndexBuilder.where()andCreateViewBuilder.as().Details
The root cause is in
DefaultQueryCompiler.sanitizeStringLiteral():src/query-compiler/default-query-compiler.ts:1819-1821Where
LIT_WRAP_REGEXis defined as/'/g(line 121). This only doubles single quotes — it does not escape backslash characters.The function is called from
appendStringLiteral()which wraps the sanitized value in single quotes:src/query-compiler/default-query-compiler.ts:1841-1845This is reached when
visitValue()encounters an immediate value node (line 525-527), which is created byImmediateValueTransformerused inCreateIndexBuilder.where():src/schema/create-index-builder.ts:266-278The
MysqlQueryCompiler(atsrc/dialect/mysql/mysql-query-compiler.ts:6-75) extendsDefaultQueryCompilerbut does not overridesanitizeStringLiteral, inheriting the backslash-unaware implementation.Exploitation mechanism:
In MySQL with the default
NO_BACKSLASH_ESCAPES=OFFsetting, the backslash character (\) acts as an escape character inside string literals. Given input\' OR 1=1 --:sanitizeStringLiteraldoubles the quote:\'' OR 1=1 --appendStringLiteralwraps:'\'' OR 1=1 --'\'as an escaped (literal) single quote, so the string content is'and the second'closes the stringOR 1=1 --is parsed as SQLPoC
To verify against a live MySQL instance:
Impact
CreateIndexBuilder.where()orCreateViewBuilder.as()can inject arbitrary SQL statements when the application uses the MySQL dialect.The attack complexity is rated High (AC:H) because exploitation requires an application to pass untrusted user input into DDL schema builder methods, which is an atypical but not impossible usage pattern. The
CreateIndexBuilder.where()docstring (line 247) notes "Parameters are always sent as literals due to database restrictions" without warning about the security implications.Recommended Fix
MysqlQueryCompilershould overridesanitizeStringLiteralto escape backslashes before doubling quotes:src/dialect/mysql/mysql-query-compiler.tsAlternatively, the library could use parameterized queries for these DDL builders where the database supports it, avoiding string literal interpolation entirely. For databases that don't support parameters in DDL statements, the dialect-specific compiler must escape all characters that have special meaning in that dialect's string literal syntax.
Severity
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:HReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Kysely: JSON-path traversal injection via unsanitized path-leg metacharacters in
JSONPathBuilder.key()/.at()CVE-2026-44635 / GHSA-pv5w-4p9q-p3v2
More information
Details
Summary
Kysely 0.28.12 added a
sanitizeStringLiteral()call insideDefaultQueryCompiler.visitJSONPathLeg(commit0a602bf, PR #1727) to fix CVE-2026-32763 (GHSA-wmrf-hv6w-mr66). The fix only doubles single quotes ('→''); it does not escape JSON-path metacharacters (.,[,],*,**,?). When attacker-controlled input flows intoeb.ref(col, '->$').key(input)or.at(input)— including type-safe code where the JSON column is shaped likeRecord<string, T>soK extends stringis the inferred type — every dot becomes a path-leg separator, letting an attacker traverse from the intended key into sibling and child fields the developer never meant to expose. The result is read access (and, in update statements, write access) to JSON sub-fields outside the intended scope across MySQL, PostgreSQL->$/->>$, and SQLite.kysely); affects MySQL, PostgreSQL->$/->>$, and SQLite dialects.kysely-org/kysely@master(73192e4, version0.28.16).kysely@0.28.16from npm.src/query-compiler/default-query-compiler.ts(lines 1611–1639, 1821–1823)src/query-builder/json-path-builder.ts(lines 93–196)src/dialect/mysql/mysql-query-compiler.ts(overridessanitizeStringLiteralbut inherits the same behaviour for path legs — escapes\and', nothing else)Vulnerable code
src/query-compiler/default-query-compiler.ts:1625-1639:src/query-compiler/default-query-compiler.ts:1821-1823:with
LIT_WRAP_REGEX = /'/g.src/query-builder/json-path-builder.ts:151-167:src/query-builder/json-path-builder.ts:169-196:At (1) the compiler emits the path-leg separator —
.for member access or[for array index. At (2) the user-supplied string is run throughsanitizeStringLiteral, which at (3) only doubles single quotes ('). Dots, brackets, asterisks, double-asterisks and question marks — every reserved character of the SQL/JSON path mini-language — pass through unmodified.At (4)
.key(K)typesKaskeyof NonNullable<O> & string. When the JSON column is typed asRecord<string, T>(a common shape for free-form metadata blobs) the inferredKis juststring, so attacker-controlled input is type-safe and does not need aKysely<any>escape hatch — this finding is broader thanGHSA-wmrf-hv6w-mr66(CVE-2026-32763), which only covered theKysely<any>case. At (5)/(6) the runtime accepts anystring | numberregardless oflegType, so a string sent into.at(...)('last'/'#-N'per the public type signature) also reaches the same emitter and can carry]to break out of the bracket.The fix at
0a602bfonly addressed the single-quote → string-literal escape. The JSON-path metacharacter set was overlooked.MysqlQueryCompiler.sanitizeStringLiteral(src/dialect/mysql/mysql-query-compiler.ts:47-51) overrides the helper to also escape backslashes — but again, it does nothing for. [ ] * ** ?.Reproduction (validated locally)
Environment:
kysely@0.28.16+better-sqlite3@​12.x, Node 22, on macOS. The PoC harness lives in/Users/admin/joplin_research/kysely-poc/.Step 1 — Compiled-SQL evidence across all three dialects
/Users/admin/joplin_research/kysely-poc/poc.mjs(no DB, just.compile()):The compiled SQL clearly shows the dot inside the user-supplied "key" being interpreted by the database as a path separator:
'$.nick'(one leg) becomes'$.nick.secret_field'(two legs). MySQL additionally accepts*as a wildcard reaching every member at the current level.Step 2 — End-to-end data disclosure on a real database
/Users/admin/joplin_research/kysely-poc/sqlite-runtime.mjssimulates a typical handler that reads one top-level field of the caller's profile:The
me.profileJSON column for user 1 is:{ "nick": "alice", "tagline": "hi", "internal": { "ssn": "111-11-1111", "token": "tok_abcdef", "admin": true } }The developer's intent: only top-level keys (
nick,tagline) are ever requested.internalis private bookkeeping.Expected vs. actual: the application invariant was "the user can only read top-level keys of their profile". The output violates that invariant —
internal.ssn,internal.token, andinternal.adminare returned even thoughinternalwas never meant to be addressable through this endpoint.The same pattern is exploitable on MySQL (where
*and**wildcards make it strictly worse — a single*enumerates every sibling at the current level in one row) and on PostgreSQL when using the->$/->>$operators (which target MySQL-style JSON-path strings on PG ≥ 17 / viajsonb_path_query).Impact
Record<string, T>— leaks data the developer believed was scoped behind the explicitly-listed key. SSNs, tokens, admin flags, internal IDs, anything stored as a nested member of the same JSON document is reachable.->$.key('*')compiles to'$.*', returning the array of every value at the current depth in one round-trip.key('**')recurses across the whole document. The fix does not strip either token.update().set(eb => eb.ref(col, '->$').key(input), value)-style writes (andjsonb_sethelpers). An attacker who can drive both the path and the value can therefore write into nested fields they should not be able to set — for example flipping anadminflag or rewriting a nested role.0a602bf(PR #1727) specifically to harden this surface. That fix removed the'(quote) primitive but left every JSON-path metacharacter alone, so the surface is still open against any caller that thought it was now safe..key(...)or.at(...). This is a recognised pattern (filter-by-field, dynamicselectfor admin dashboards, Strapi-style JSON-blob columns); it is not a default kysely behaviour but is plausibly common. The vulnerable path is also exercised any time a developer writesdb as Kysely<any>(covered by the olderGHSA-wmrf-hv6w-mr66advisory) — but unlike that advisory, the bug here triggers in fully-typed code onRecord<string, T>columns.Suggested fix
Treat path legs as a structured emission, not a string-literal escape. The narrowest safe patch is a dedicated
sanitizeJSONPathLegthat only emits a known-good character set per leg type and rejects everything else, since JSON-path quoting differs by dialect (MySQL allows"…"-quoted member names; SQLite is more permissive but still has a grammar; PostgreSQLjsonpathis strict).For dialect-specific behaviour (MySQL
"…"-quoted members, SQLite bracket-quoted), each dialect compiler should override the helper and apply the appropriate quoting + double-the-quote rule, the same waysanitizeIdentifieralready does.Consider also: parameterise JSON paths whenever the dialect supports it (PostgreSQL
jsonb_path_query($1, $2), MySQLJSON_EXTRACT(?, ?)), so attacker-controlled keys are bound, not concatenated. Add a regression test totest/node/src/json-traversal.test.tsasserting thateb.ref('c','->$').key('a.b').compile().sqlis either rejected, or emits MySQL'$."a.b"'/ SQLite'$.["a.b"]'(quoted-member form), and explicitly differs fromkey('a').key('b').A backstop hardening: tighten the
.at()runtime to accept onlynumber | 'last' | '#-${digits}'(matching the type signature), and tighten.key()to only accept strings that matchkeyof Oat runtime whenOis statically known.Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:NReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Release Notes
kysely-org/kysely (kysely)
v0.28.17: 0.28.17Compare Source
Hey 👋
A small batch of bug fixes. Please report any issues. 🤞😰🤞
0.29 is right around the corner. Try the latest RC version!
🚀 Features
🐞 Bugfixes
.key(...)and.at(...)against SQL injections and exfiltrations. by @igalklebanov in #1804📖 Documentation
📦 CICD & Tooling
🐤 New Contributors
What's Changed
Full Changelog: kysely-org/kysely@v0.28.16...v0.28.17
v0.28.16: 0.28.16Compare Source
Hey 👋
A small batch of bug fixes. Please report any issues. 🤞😰🤞
0.29 is getting closer btw. 🌶️
🚀 Features
🐞 Bugfixes
FilterObjectallows any defined value when query context has no tables (TBisnever). by @igalklebanov in #1791📖 Documentation
db646ac559714469989155a0f14b📦 CICD & Tooling
2301610f4f1d9eab6d00e521156bverifyDepsBeforeRunto "prompt". by @igalklebanov in20548bc🐤 New Contributors
What's Changed
Full Changelog: kysely-org/kysely@v0.28.15...v0.28.16
v0.28.15: 0.28.15Compare Source
Hey 👋
The introduction of dehydration in JSON functions/helpers caused an unexpected bug for consumers that have some columns defined as
'${number}', e.g.'1' | '2'(also when wrapped inColumnTypeor similar). Such columns, when participating in a JSON function/helper would dehydrate tonumberinstead of staying asstring.Why dehydrate numeric strings to numbers in the first place? Select types in
kyselydescribe the data after underlying driver's (e.g.pg) data transformation. Some drivers transform numeric columns to strings to be safe. When these columns participate in JSON functions, they lose original column data types - drivers don't know they need to transform tostring- they return as-is.This release introduces a special helper type that wraps your column type definition and tells
kyselyto NOT dehydrate it in JSON functions/helpers.🚀 Features
NonDehydrateable<T>to allow opt-out from dehydration in JSON functions/helpers. by @igalklebanov in #1697🐞 Bugfixes
PostgreSQL 🐘
📖 Documentation
📦 CICD & Tooling
🐤 New Contributors
Full Changelog: kysely-org/kysely@v0.28.14...v0.28.15
v0.28.14: 0.28.14Compare Source
Hey 👋
A small batch of bug fixes. Please report any issues. 🤞😰🤞
🚀 Features
🐞 Bugfixes
MySQL 🐬
\\') are used. by @igalklebanov in #1754 &054e801📖 Documentation
📦 CICD & Tooling
9e02f3b🐤 New Contributors
Full Changelog: kysely-org/kysely@v0.28.13...v0.28.14
v0.28.13: 0.28.13Compare Source
Hey 👋
A small batch of bug fixes. Please report any issues. 🤞😰🤞
🚀 Features
🐞 Bugfixes
sideEffects: falsein rootpackage.jsonresulting in bigger bundles in various bundlers. by @igalklebanov in #1746Insertableallows non-objects when a table has no required columns. by @igalklebanov in #1747PostgreSQL 🐘
ON COMMITclause not being output when using.as(query)inCREATE TABLEqueries. by @igalklebanov in #1748📖 Documentation
📦 CICD & Tooling
tsconfig.jsonfor TypeScript native. by @igalklebanov in #1749🐤 New Contributors
Full Changelog: kysely-org/kysely@v0.28.12...v0.28.13
v0.28.12: 0.28.12Compare Source
Hey 👋
A small batch of bug fixes. Please report any issues. 🤞😰🤞
🚀 Features
🐞 Bugfixes
eb.ref(col, '->$').key(key)is injectable. by @igalklebanov in #1727MySQL 🐬
mysql2@​v3.18.2support #1722 by @fenichelar in #1729📖 Documentation
📦 CICD & Tooling
🐤 New Contributors
Full Changelog: kysely-org/kysely@v0.28.11...v0.28.12
Configuration
📅 Schedule: (UTC)
🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.
♻ Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.
🔕 Ignore: Close this PR and you won't be reminded about this update again.
This PR was generated by Mend Renovate. View the repository job log.